1 /**
2 The following examples come from
3 $(LINK http://zetcode.com/db/sqlite/constraints/).
4 Even though it is a SQLite tutorial the point is to show how to use this package
5 which does not have to be just SQLite.
6  */
7 module test.examples_foreign_key;
8 
9 version(D_Ddoc)
10 {
11     ///
12     class BlankClassSoDocsWillBeGenerated { }
13 }
14 
15 
16 /**
17 This example is for the FOREIGN KEY constraints. The tables
18 in SQL can be created by
19 $(D $(D $(D sql
20 CREATE TABLE Authors
21 (
22     AuthorId INTEGER NOT NULL PRIMARY KEY,
23     Name TEXT
24 );
25 
26 CREATE TABLE Books
27 (
28     BookId INTEGER NOT NULL PRIMARY KEY,
29     Title TEXT,
30     AuthorId INTEGER,
31     FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId)
32 );
33 
34 )))
35 
36 The Books table now must have values in Books.AuthorId that are in
37 Authors.AuthorId. By default if Authors deletes or updates its AuthorId
38 and Books references the AuthorId, an exception is thrown. If you would
39 like Books to cascade the effects instead you would create the Books table like
40 $(D $(D $(D sql
41 CREATE TABLE Books
42 (
43     BookId INTEGER NOT NULL PRIMARY KEY,
44     Title TEXT,
45     AuthorId INTEGER,
46     FOREIGN KEY (AuthorId) REFERENCES Authors(AuthorId)
47     ON DELETE CASCADE
48     ON UPDATE CASCADE
49 );
50 
51 )))
52 
53 This package can do any of the update or delete rules.
54 For more rules information look at $(WIKI constraints, Rule).
55 
56 Below I will create the classes. I will use the cascaded Books for this example.
57  */
58 unittest
59 {
60     import db_constraints;
61 
62     class Author
63     {
64         private int _AuthorId;
65         @PrimaryKeyColumn @NotNull
66         @property int AuthorId()
67         {
68             return _AuthorId;
69         }
70         @property void AuthorId(int value)
71         {
72             setter(_AuthorId, value);
73         }
74         private string _Name;
75         @property string Name()
76         {
77             return _Name;
78         }
79         @property void Name(string value)
80         {
81             setter(_Name, value);
82         }
83         this(int AuthorId_, string Name_)
84         {
85             this._AuthorId = AuthorId_;
86             this._Name = Name_;
87             initializeKeyedItem();
88         }
89 
90         // we must define dup() since we are going
91         // to change AuthorId which is our implied
92         // clustered index
93         Author dup()
94         {
95             return new Author(this._AuthorId, this._Name);
96         }
97 
98         mixin KeyedItem!();
99     }
100     class Authors
101     {
102         mixin KeyedCollection!(Author);
103     }
104 
105     // adding in this function so we can get multiple
106     // author records at once
107     Authors GetAuthorsFromDB()
108     {
109         return new Authors([
110                            new Author(1, "Jane Austen"),
111                            new Author(2, "Leo Tolstoy"),
112                            new Author(3, "Joseph Heller"),
113                            new Author(4, "Charles Dickens")
114                            ]);
115     }
116 
117 
118     // we could put Books in a different file
119     // to do that you would just need to import the file where Authors is.
120 
121     // attach the foreign key constraint attribute to the singular class
122     @ForeignKeyConstraint!(
123         ["AuthorId"], /* Book column */
124         "Authors", /* referenced table which is Authors in this case */
125         ["AuthorId"], /* referenced column which is Authors.AuthorId */
126         Rule.cascade, /* what to do when we update Authors.AuthorId */
127         Rule.cascade) /* what to do when we delete Authors.AuthorId */
128     class Book
129     {
130         private int _BookId;
131         @PrimaryKeyColumn @NotNull
132         @property int BookId()
133         {
134             return _BookId;
135         }
136         @property void BookId(int value)
137         {
138             setter(_BookId, value);
139         }
140         private string _Title;
141         @property string Title()
142         {
143             return _Title;
144         }
145         @property void Title(string value)
146         {
147             setter(_Title, value);
148         }
149         private int _AuthorId;
150         @property int AuthorId()
151         {
152             return _AuthorId;
153         }
154         @property void AuthorId(int value)
155         {
156             setter(_AuthorId, value);
157         }
158         this(int BookId_, string Title_, int AuthorId_)
159         {
160             this._BookId = BookId_;
161             this._Title = Title_;
162             this._AuthorId = AuthorId_;
163             initializeKeyedItem();
164         }
165 
166         mixin KeyedItem!();
167     }
168 
169     class Books
170     {
171         mixin KeyedCollection!(Book);
172     }
173     Books GetBooksFromDB()
174     {
175         return new Books([
176                          new Book(1, "Emma", 1),
177                          new Book(2, "War and Peace", 2),
178                          new Book(3, "Catch XII", 3),
179                          new Book(4, "David Copperfield", 4),
180                          new Book(5, "Good as Gold", 3),
181                          new Book(6, "Anna Karenia", 2)
182                          ]);
183     }
184 
185 
186     // we will get both collections
187     // and then associate authors to books
188 
189     // ON UPDATE CASCADE
190     {
191         auto authors = GetAuthorsFromDB();
192         auto books = GetBooksFromDB();
193 
194         // when we associate authors to books
195         // there should be no exceptions since
196         // we are starting with correct data
197         import std.exception : assertNotThrown, assertThrown;
198         assertNotThrown!ForeignKeyException(books.authors = authors);
199         // books.authors is a write-only property made by
200         // mixin KeyedCollection!(Book);
201         // you can also set books.authors = null when you want to
202         // remove the association
203 
204         // if you recall from before we can use the primary key to
205         // search our collections easily. In Books, Emma has BookId 1.
206         assert(books[1].Title == "Emma");
207         assert(books[1].BookId == 1);
208 
209         // Emma is written by Jane Austen and
210         // in authors Jane Austen has AuthorId 1
211         assert(authors[1].Name == "Jane Austen");
212         assert(authors.contains(books[1].AuthorId));
213         assert(authors[1].AuthorId == books[1].AuthorId);
214 
215         // lets say somehow we changed Jane Austen to have AuthorId 5
216         // since we have on update cascade for books we would expect
217         // Emma to get AuthorId 5
218         authors[1].AuthorId = 5;
219         // this will also change authors to no longer have key 1
220         assert(authors.contains(5) && !authors.contains(1));
221         // Emma still has BookId 1 since that did not change but
222         // should have AuthorId 5
223         assert(books[1].Title == "Emma");
224         assert(books[1].AuthorId == 5);
225 
226         // We were able to change Jane Austen's AuthorId since we
227         // defined dup in Author. We did not define dup in Books
228         // which means if we change BookId we should expect a
229         // KeyedException
230         assertThrown!KeyedException(books[1].BookId = 7);
231 
232         // it is good but not necessary to set books.authors to null when you
233         // leave scope just to disconnect signals and associations
234         books.authors = null;
235     }
236 
237     // ON DELETE CASCADE
238     {
239         auto authors = GetAuthorsFromDB();
240         auto books = GetBooksFromDB();
241         books.authors = authors;
242 
243         // we have 4 authors
244         assert(authors.length == 4);
245         // and 6 books
246         assert(books.length == 6);
247 
248         import std.algorithm : count;
249         // there are two books that have author id 3
250         auto booksWithAuthorId3 =
251             books.byValue.count!((a, b) => a.AuthorId == b)(3);
252 
253         assert(booksWithAuthorId3 == 2);
254 
255         // this means if we delete AuthorId 3 from authors and
256         // we have on delete cascade for books we should expect
257         // books to have length 4 and authors to have length 3
258         authors.remove(3);
259         assert(authors.length == 3);
260         assert(books.length == 4);
261 
262         booksWithAuthorId3 = books.byValue.count!((a, b) => a.AuthorId == b)(3);
263         assert(booksWithAuthorId3 == 0);
264 
265         books.authors = null;
266     }
267 
268     // ON DELETE RESTRICT
269     {
270         import std.exception : assertThrown, assertNotThrown;
271         auto authors = GetAuthorsFromDB();
272         auto books = GetBooksFromDB();
273         books.authors = authors;
274 
275         // you can change the on update and on delete rule for your foreign key
276         // by using the foreign key name and _UpdateRule or _DeleteRule...we
277         // did not name ours so it got the default fk_Book_Authors
278 
279         // lets say we want to restrict authors deletion now
280         books.fk_Book_Authors_DeleteRule = Rule.restrict;
281 
282         // since we have 2 records in books that reference AuthorId 3 we should
283         // get an exception when deleting AuthorId 3 from authors
284         assertThrown!ForeignKeyException(authors.remove(3));
285 
286         // now we can unreference our table and remove 3 without errors
287         books.authors = null;
288         assertNotThrown!ForeignKeyException(authors.remove(3));
289 
290         // but now when we try to re-associate we will get an exception
291         assertThrown!ForeignKeyException(books.authors = authors);
292     }
293 }